package pico.picoScannerParser;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;

import pico.PicoVariable;

import ebnf.ebnfGrammar.Expression;
import ebnf.ebnfGrammar.NonTerminal;
import ebnf.ebnfScannerParser.EBNFParser;
import ebnf.ebnfScannerParser.EBNFToken;

/**
 *  Parser voor PicoParser
 *  
 * @author Sander Meijer en Bart Wentink
 *
 */
public class PicoParser implements PicoTokenKinds {

	private PicoScanner scanner;
	private PicoToken lookAhead;

	// The datastructure 
	private ArrayList<NonTerminal> dataStructure;

	// String of what has been parsed so far
	private String parsed;
	

	// The hashmap variables contains all found variables, 
	// declare states if it is allowed to add items to it.
	private HashMap<String, PicoVariable> variables;
	private boolean declare = false;
	
	// Three variables needed to declare a variable.
	private String variableType;
	private String name;
	private String variableValue;
	
	// Three variables needed to handle an assstat nonterminal
	private boolean assigning = false;
	private PicoVariable picoVar;
	private String assigningVariableValue;
	
	// the variable that states if it is allowed to check if variable are declared
	private boolean allowedToCheck = false;
	
	private String UNKNOWN_VARIABLE = "The variable \"%s\" is unknown and therefor not allowed to be used here.";
	private String UNDECLARED_VARIABLE = "The variable \"%s\" has not been declared and therefore is not allowed to be used here.";
	private boolean SHOW_DATA = false;

	/**
	 * Constructor
	 * 
	 * @param debugging
	 */
	public PicoParser(Boolean showData) {
		SHOW_DATA = showData;
		variables = new HashMap<String, PicoVariable>();
		name = "";
		variableType = "";
		variableValue = "";
	}

	/**
	 * Parses the given file name, path_pico_code, with the given datastructure, path_datastructure.
	 * @param {@link String} path_datastructure
	 * @param {@link String} path_pico_code
	 * @return {@link String}
	 */
	public String parse(String path_datastructure, String path_pico_code) {
		resetParser();
		
		try {
			//Sets up the datastructure
			EBNFParser parser = new EBNFParser(false);
			parser.parse(path_datastructure);
			dataStructure = parser.getDatastructure();

			// loads in the file to parse
			FileInputStream fis = new FileInputStream(new File(path_pico_code));
			this.scanner = new PicoScanner(new BufferedReader(
					new InputStreamReader(fis)));

			// Initialize lookAhead by getting the first token from the parser
			acceptIt();

			if (SHOW_DATA) {
				System.out.println(parser.toString() + "\n\n");
			}
			parseNonTerminal(dataStructure.get(0));
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
		
		if(SHOW_DATA){
			System.out.println("Variables");
			for(PicoVariable var : variables.values()){
				System.out.println(var.toString());
			}
		}
		
		return parsed;
	}

	/**
	 * Resets the parser to empy and false values.
	 */
	public void resetParser() {
		parsed = "";
		assigning = false;
		allowedToCheck = false;
		variables = new HashMap<String, PicoVariable>();
	}

	/**
	 * Throws a run time error when there is a error during the parsing.
	 * 
	 * @param {@link String} nonterminal
	 * @param {@link String} message
	 * @throws RuntimeException
	 */
	private void parseError(String nonterminal, String message) {
		System.err.println();
		System.err.println("parse error in <" + nonterminal + "> : " + message);
		System.err.print("lookahead '" + lookAhead.getText() + "' ");
		System.err.print("(kind=" + lookAhead.getKind() + ") ");
		System.err.print("at " + lookAhead.getLine() + "/"
				+ lookAhead.getColumn());
		System.err.println();
		throw new RuntimeException();
	}

	/**
	 * Throws an error when there is a problem with the declared variable.
	 * @param {@link String} message
	 * @param {@link String} variableName
	 * @throws RuntimeException
	 */
	private void declartionError(String message, String variableName){
		System.err.println();
		System.err.printf(message, variableName);
		System.err.println("\nSo far parsed the following:");
		System.err.println(parsed);
		System.err.println("Known Variables:");
		for(PicoVariable var : variables.values()){
			System.err.println(var.toString());
		}
		System.err.println();
		throw new RuntimeException();
	}

	/**
	 * accepts a token by putting the next token on lookAhead. </br>
	 */
	private void acceptIt() {
		try {
			lookAhead = scanner.nextToken();
		} catch (IOException e) {
			e.printStackTrace();
			throw new RuntimeException();
		}
	}

	/**
	 * Checks if the current token can be accepted, if not throws an error.
	 * 
	 * @param {@link String} nonterminal
	 * @param {@link Integer} kind
	 */
	private void accept(String nonterminal, String kind) {
		if (!lookAhead.getKind().equals(kind)) {
			parseError(nonterminal, "kind=" + kind + " expected");
		} else {
			parsed += (lookAhead.getText() + " ");
			acceptIt();
		}
	}
		
	/**
	 * Parsed the given NonTerminal
	 * @param {@link NonTerminal} nonTerm
	 */
	private void parseNonTerminal(NonTerminal nonTerm) {
		if (nonTerm.getText().equals("decl")) {
			// When working on nonTerminal decl, declare must be true.
			declare = true;
		}
		
		if (nonTerm.getText().equals("stats")) {
			// Adds the last variable before beginning to check the rest of the code
			addVariable();
			allowedToCheck = true;
		}
		
		if(assigning && nonTerm.getText().contains("nestednonterm")){
			// When assigning and encountering a nestednonterm,
			// the entire value has been found and needs to be
			// added to the variable
			picoVar.setValue(assigningVariableValue);
			assigning = false;
			picoVar = null;
		}
		
		if (nonTerm.getText().equals("assstat")) {
			// Has found an assstat and thus it is not allowed
			// to check if the variable has been declared already
			assigning = true;
			allowedToCheck = false;
			assigningVariableValue = "";
		}
		
		if(nonTerm.getText().equals("nestednonterm_1")){
			// Found next variable and thus needs to add the current one.
			addVariable();
		}
		
		// Runs through all the expressions of the NonTerminal and 
		// checks if it is not empty. 
		for (Expression expr : nonTerm.getExpressions()) {
			if (!expr.isEmpty()) {
				
				// If the first is a nonterminal, it will be put in tempTerm
				// If tempterm remains null, and the first equals the lookAhead,
				// the expression will be parsed
				EBNFToken first = expr.getFirst();
				NonTerminal tempTerm = getNonTerm(first);
				
				if (tempTerm == null) {
					if (first.getText().equals(lookAhead.getKind())) {						
						parseExpression(expr);
					}
				// if tempTerm is not null, it will be checked if it is the correct nonTerminal
				// if it is, the expression will be parsed.
				} else if (isCorrect(tempTerm)) {
					parseExpression(expr);
				}
			}
		}
	}

	/**
	 * Parsed the given expression
	 * @param {@link Expression} expr
	 */
	private void parseExpression(Expression expr) {
		
		//runs through all the tokens of the expression	
		// checks if the token is a non terminal
		// if it is a nonterminal, it will begin to parse that
		// else it will be accepted
		for (EBNFToken tok : expr.getTokens()) {

			NonTerminal tempTerm = getNonTerm(tok);
			if (tempTerm != null) {
				parseNonTerminal(tempTerm);
			} else {
				if(declare && !name.equals("") && !variableType.equals("") && !lookAhead.getText().equals(INITMARK)){
					// Name and type have been found and we are not at the initmark
					// thus we have found a part of the variable value
					variableValue += lookAhead.getText();
				}
				if(declare && name.equals("") && !variableType.equals("")){
					// we have declared the type and the name is still empty
					// therefor we have now found the name
					name = lookAhead.getText();
				}
				if(declare && variableType.equals("")){
					// We are declaring and the type is still empty,
					// therefore we have now found the type
					variableType = lookAhead.getText();
				}
				
				if(assigning && !lookAhead.getText().equals(IS) && picoVar != null){
					// We are parsing an assstat and are not at the IS, 
					// the picoVar has also been found
					// thus we have found a part of the variable value
					assigningVariableValue += lookAhead.getText();
				}
				
				if(allowedToCheck && lookAhead.getKind() == P_IDENTIFIER){
					//We are allowed to check and the lookAhead is an p_identifier
					// if we can find the variable and it is declared, nothing happens
					// else an error will be thrown.
					PicoVariable tempVar = getVariable(lookAhead.getText());
					if(!tempVar.isDeclared()){
						declartionError(UNDECLARED_VARIABLE, lookAhead.getText());
					}
				}
				if(assigning && picoVar == null){
					// we are parsing an assstat and the picoVar is still null
					// thus we we must find the variable for the lookAhead
					picoVar = getVariable(lookAhead.getText());
				}
				accept(lookAhead.getKind(), tok.getText());
			}
		}
	}
	
	/**
	 * Adds a variable to the variable hashmap.
	 */
	private void addVariable() {
		if(name.length() != 0){
			declare = false;

			PicoVariable var = new PicoVariable(name, variableValue, variableType);
			variables.put(name, var);

			name = "";
			variableValue = "";
			variableType = "";
		}
	}
	
	/**
	 * Gets the variable with the given name
	 * @param {@link String} name
	 * @return {@link PicoVariable} picoVar
	 * @throws declarationError when the name cannot be found.
	 */
	private PicoVariable getVariable(String name){
		PicoVariable retVar = null;
	
		retVar = variables.get(name);
	
		if(retVar == null){
			declartionError(UNKNOWN_VARIABLE, name);
		}
		return retVar;
	}

	/**
	 * gets the NonTerminal for the given token. </br>
	 * Returns null when not found, and the token is thus a not a NonTerminal
	 * @param {@link EBNFToken token
	 * @return {@link NonTerminal} nonTerm
	 */
	private NonTerminal getNonTerm(EBNFToken token) {
		for (NonTerminal nonTerm : dataStructure) {

			if (nonTerm.getText().equals(token.getText())) {
				return nonTerm;
			}
		}
		return null;
	}

	/**
	 * Checks if the current NonTerm is used in the correct spot, </br>
	 * if it finds a NonTerminal it will be done for that NonTerminal too.
	 * @param {@link NonTerminal} nonterm
	 * @return {@link Boolean} correct
	 */
	private boolean isCorrect(NonTerminal nonterm) {
		boolean correct = false;
		
		//for each expression it will check if the first is a NonTerminal
		// if it is, it will call on isCorrect again
		// else it will check if the first equals the lookahead
		for (Expression expr : nonterm.getExpressions()) {
			NonTerminal temp = getNonTerm(expr.getFirst());
			if (temp == null) {
				if (expr.getFirst().getText().equals(lookAhead.getKind())) {
					correct = true;
				}
			} else {
				if (!correct) {
					correct = isCorrect(temp);
				}
			}
		}
		return correct;
	}
}
